package ar.uba.dc.tl.mylanga.interpreter;

import ar.uba.dc.tl.mylanga.analysis.AnalysisAdapter;
import ar.uba.dc.tl.mylanga.node.AAdditionNumericExpression;
import ar.uba.dc.tl.mylanga.node.AAndBooleanExpression;
import ar.uba.dc.tl.mylanga.node.AAssignmentStatement;
import ar.uba.dc.tl.mylanga.node.ABooleanExpression;
import ar.uba.dc.tl.mylanga.node.ACompoundStatement;
import ar.uba.dc.tl.mylanga.node.ADivisionNumericExpression;
import ar.uba.dc.tl.mylanga.node.AEqualRelationalExpression;
import ar.uba.dc.tl.mylanga.node.AFunctionCall;
import ar.uba.dc.tl.mylanga.node.AFunctionCallNumericExpression;
import ar.uba.dc.tl.mylanga.node.AFunctionDefinition;
import ar.uba.dc.tl.mylanga.node.AGreaterThanOrEqualRelationalExpression;
import ar.uba.dc.tl.mylanga.node.AGreaterThanRelationalExpression;
import ar.uba.dc.tl.mylanga.node.AIfThenElseStatement;
import ar.uba.dc.tl.mylanga.node.AIfThenStatement;
import ar.uba.dc.tl.mylanga.node.ALessThanOrEqualRelationalExpression;
import ar.uba.dc.tl.mylanga.node.ALessThanRelationalExpression;
import ar.uba.dc.tl.mylanga.node.AMultiplicationNumericExpression;
import ar.uba.dc.tl.mylanga.node.ANegationBooleanExpression;
import ar.uba.dc.tl.mylanga.node.ANotEqualRelationalExpression;
import ar.uba.dc.tl.mylanga.node.ANumberNumericExpression;
import ar.uba.dc.tl.mylanga.node.ANumericExpression;
import ar.uba.dc.tl.mylanga.node.AOrBooleanExpression;
import ar.uba.dc.tl.mylanga.node.AParameter;
import ar.uba.dc.tl.mylanga.node.APlotCommand;
import ar.uba.dc.tl.mylanga.node.AProgram;
import ar.uba.dc.tl.mylanga.node.ARelationalBooleanExpression;
import ar.uba.dc.tl.mylanga.node.AReturnStatement;
import ar.uba.dc.tl.mylanga.node.ASubtractionNumericExpression;
import ar.uba.dc.tl.mylanga.node.AUnaryMinusNumericExpression;
import ar.uba.dc.tl.mylanga.node.AVariableNumericExpression;
import ar.uba.dc.tl.mylanga.node.AWhileStatement;
import ar.uba.dc.tl.mylanga.node.PBooleanExpression;
import ar.uba.dc.tl.mylanga.node.PFunctionDefinition;
import ar.uba.dc.tl.mylanga.node.PNumericExpression;
import ar.uba.dc.tl.mylanga.node.PParameter;
import ar.uba.dc.tl.mylanga.node.PPlotCommand;
import ar.uba.dc.tl.mylanga.node.PProgram;
import ar.uba.dc.tl.mylanga.node.PStatement;
import ar.uba.dc.tl.mylanga.node.Start;
import ar.uba.dc.tl.mylanga.node.TNumber;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;

/*
 * The InterpreterSeparateStacks class interprets the program by traversing the AST.
 *
 * This implementation uses two different execution stacks. One stack is used
 * exclusively for containing intermediate computations and the other is used
 * for holding locals/parameters and saving interpreter registers on function call.
 * This implementation does not need to know the number of locals while creating
 * a function call frame because the local stack is not used for holding intermediate results.
 */
public class InterpreterSeparateStacks extends AnalysisAdapter {

	private static final int LOCAL_NEXT_DELTA = 1;
	private static final int LOCAL_FIRST_INDEX = 1;
	private static final int PARAMETER_NEXT_DELTA = -1;
	private static final int PARAMETER_FIRST_INDEX = -2;

	int bp;
	boolean returned;
	private int nextLocal;
	private int parameterIndex;
	private List<Point> points;
	private ExecutionStack executionStack;
	private ExecutionStack localsAndParametersStack;
	private Map<String, Object> namedConstants;
	private Stack<Map<String, Integer>> localsBindings;
	private Map<String, AFunctionDefinition> functionDefinitions;

	public InterpreterSeparateStacks() {
		this.initializeNamedConstants();
	}

	@Override
	public void caseStart(Start node) {
		// Initialize Interpreter state.
		this.points = new ArrayList<>();
		this.localsBindings = new Stack<>();
		this.functionDefinitions = new HashMap<>();
		this.executionStack = new ExecutionStack();
		this.localsAndParametersStack = new ExecutionStack();

		// Get the program.
		PProgram program = node.getPProgram();

		// Execute the program.
		program.apply(this);
	}

	@Override
	public void caseAProgram(AProgram node) {
		PPlotCommand plotCommand = node.getCommand();
		List<PFunctionDefinition> functionDefinitions = node.getFunctionDefinitions();

		// Collect all the function definitions.
		for (PFunctionDefinition functionDefinition : functionDefinitions) {
			functionDefinition.apply(this);
		}

		// Execute the plot command.
		plotCommand.apply(this);
	}

	@Override
	public void caseAFunctionDefinition(AFunctionDefinition node) {
		String functionName = node.getName().getText();

		// Store the function definition for latter use.
		this.functionDefinitions.put(functionName, node);
	}

	@Override
	public void caseAPlotCommand(APlotCommand node) {
		// Get the for clause parameter name.
		String parameterName = node.getParameter().getText();

		// Create a special initial frame on the stack
		// containing the for clause parameter. We use this
		// frame to evaluate the for arguments.
		// The parameter has a dummy value.
		this.createInitialFrame(parameterName, 0);

		// Execute and store the from value.
		node.getFrom().apply(this);
		double from = this.executionStack.popDouble();

		// Execute and store the by value.
		node.getBy().apply(this);
		double by = this.executionStack.popDouble();

		// Execute and store the to value.
		node.getTo().apply(this);
		double to = this.executionStack.popDouble();

		// Deallocate the initial frame.
		this.deallocateInitialFrame();

		// Execute the plot command functions for all the values
		// between from and to, incrementing by by each time.
		for (double value = from; value <= to; value += by) {
			// Create a special initial frame on the stack
			// containing the for clause parameter.
			this.createInitialFrame(parameterName, value);

			// Execute the first function of the plot command.
			node.getFirst().apply(this);
			double first = this.executionStack.popDouble();

			// Execute the second function of the plot command.
			node.getSecond().apply(this);
			double second = this.executionStack.popDouble();

			// Create a point with the two generated values.
			Point point = new Point(first, second);

			// Save the point in the results collection.
			this.points.add(point);

			// Deallocate the initial frame.
			this.deallocateInitialFrame();
		}
	}

	/*
	 * This method is called any time a function call is encountered.
	 */
	@Override
	public void caseAFunctionCall(AFunctionCall node) {
		String functionName = node.getName().getText();

		// Lookup the function definition in the function cache.
		AFunctionDefinition functionDefinition = this.functionDefinitions.get(functionName);

		// Push parameters, save returned and base registers.
		this.allocateFrame(functionDefinition, node);

		// The returned register is used to signal if a return statement
		// has been executed and to return from the function immediately.
		this.returned = false;

		// Execute the body statements.
		functionDefinition.getBody().apply(this);

		// Save function return value.
		Object result = this.executionStack.pop();

		// Clean up the stack.
		this.deallocateFrame(functionDefinition);

		// Leave the function return value at stack top.
		this.executionStack.push(result);
	}

	@Override
	public void caseAParameter(AParameter node) {
		this.localsBindings.peek().put(node.getName().getText(), this.parameterIndex);
		this.parameterIndex += PARAMETER_NEXT_DELTA;
	}

	@Override
	public void caseAAssignmentStatement(AAssignmentStatement node) {
		String lvalueName = node.getLeftValue().getText();

		// Because the language does not require variable declaration
		// we need to take care that an assignment might create a new local.
		// The number of locals is computed before starting to execute
		// the function (during frame creation), and the locals must fit
		// in the allocated space.
		if (!this.localsBindings.peek().containsKey(lvalueName)) {
			this.localsAndParametersStack.push(0.0);
			this.localsBindings.peek().put(lvalueName, this.nextLocal);
			this.nextLocal += LOCAL_NEXT_DELTA;
		}

		// At this point, the lvalue has been bind to some
		// stack position. The variable offset contains the
		// offset from the base pointer where the local is
		// located in the stack.
		int offset = this.localsBindings.peek().get(lvalueName);

		// Evaluate the expression, leaving the result on the stack top.
		node.getRightValue().apply(this);

		// Remove stack top.
		Object value = this.executionStack.pop();

		// Store it in the local.
		this.localsAndParametersStack.set(this.bp + offset, value);
	}

	@Override
	public void caseACompoundStatement(ACompoundStatement node) {
		List<PStatement> statements = node.getStatements();

		// Execute the statements checking if a return has occurred.
		for (int index = 0; !this.returned && index < statements.size(); ++index) {
			PStatement statement = statements.get(index);

			statement.apply(this);
		}
	}

	@Override
	public void caseAIfThenStatement(AIfThenStatement node) {
		// Evaluate condition.
		node.getCondition().apply(this);

		// Pop condition result from the stack.
		boolean result = this.executionStack.popBoolean();

		// If result then execute the then statements.
		if (result) {
			node.getThenStatements().apply(this);
		}
	}

	@Override
	public void caseAIfThenElseStatement(AIfThenElseStatement node) {
		// Evaluate condition.
		node.getCondition().apply(this);

		// Pop condition result from the stack.
		boolean result = this.executionStack.popBoolean();

		// If result then execute the then statements. If
		// not result, then execute the else statements.
		if (result) {
			node.getThenStatements().apply(this);
		} else {
			node.getElseStatements().apply(this);
		}
	}

	@Override
	public void caseAReturnStatement(AReturnStatement node) {
		// Evaluate the expression leaving the value on the stack.
		node.getNumericExpression().apply(this);

		// Signal a return happened.
		this.returned = true;
	}

	@Override
	public void caseAWhileStatement(AWhileStatement node) {
		PStatement body = node.getBody();
		PBooleanExpression condition = node.getCondition();

		// Evaluate the condition.
		condition.apply(this);

		// Pop condition result from the stack.
		boolean result = this.executionStack.popBoolean();

		// While the result is true and a return has not been executed.
		while (result && !this.returned) {
			// Execute the while body.
			body.apply(this);

			// Evaluate the condition again.
			condition.apply(this);

			// Update the result.
			result = this.executionStack.popBoolean();
		}
	}

	@Override
	public void caseANumericExpression(ANumericExpression node) {
		node.getNumericExpression().apply(this);
	}

	@Override
	public void caseANumberNumericExpression(ANumberNumericExpression node) {
		// Convert the number text into a double.
		double value = Double.parseDouble(node.getNumber().getText());

		// Leave the value at the stack top.
		this.executionStack.push(value);
	}

	@Override
	public void caseAUnaryMinusNumericExpression(AUnaryMinusNumericExpression node) {
		// Execute the underlying numeric expression.
		node.getNumericExpression().apply(this);

		// Pop the value from the stack top.
		double value = this.executionStack.popDouble();

		// Push -value into the stack top.
		this.executionStack.push(-value);
	}

	@Override
	public void caseAAdditionNumericExpression(AAdditionNumericExpression node) {
		// Execute both addends expressions.
		node.getLeft().apply(this);
		node.getRight().apply(this);

		// Pop the addends from the stack top and add them.
		double value = this.executionStack.popDouble() + this.executionStack.popDouble();

		// Push the addition into the stack top.
		this.executionStack.push(value);
	}

	@Override
	public void caseASubtractionNumericExpression(ASubtractionNumericExpression node) {
		// Evaluate both operands.
		node.getLeft().apply(this);
		node.getRight().apply(this);

		// Pop the subtrahend from the stack top.
		double subtrahend = this.executionStack.popDouble();

		// Pop the minuend from the stack top.
		double minuend = this.executionStack.popDouble();

		// Calculate the difference.
		double value = minuend - subtrahend;

		// Push the difference into the stack top.
		this.executionStack.push(value);
	}

	@Override
	public void caseAMultiplicationNumericExpression(AMultiplicationNumericExpression node) {
		// Evaluate both factors.
		node.getLeft().apply(this);
		node.getRight().apply(this);

		// Pop the factors from the stack and multiply them.
		double value = this.executionStack.popDouble() * this.executionStack.popDouble();

		// Push the product into the stack top.
		this.executionStack.push(value);
	}

	@Override
	public void caseADivisionNumericExpression(ADivisionNumericExpression node) {
		// Evaluate both operands.
		node.getLeft().apply(this);
		node.getRight().apply(this);

		// Pop the divisor from the stack.
		double divisor = this.executionStack.popDouble();

		// Pop the dividend from the stack.
		double dividend = this.executionStack.popDouble();

		// Calculate the division.
		double value = dividend / divisor;

		// Push the division into the stack top.
		this.executionStack.push(value);
	}

	@Override
	public void caseAFunctionCallNumericExpression(AFunctionCallNumericExpression node) {
		node.getFunctionCall().apply(this);
	}

	@Override
	public void caseAVariableNumericExpression(AVariableNumericExpression node) {
		String variableName = node.getIdentifier().getText();

		// Get the variable offset. We don't need to create an entry
		// if there is none, because we do not allow to used
		// uninitialized variables. But we need to take into account
		// that it can be a named constant like pi.
		if (this.localsBindings.peek().containsKey(variableName)) {
			// This is a normal variable
			int offset = this.localsBindings.peek().get(variableName);

			// Retrieve the variable from the stack.
			Object value = this.localsAndParametersStack.get(this.bp + offset);

			// Push the variable value into the stack top.
			this.executionStack.push(value);
		} else {
			// This must be a named constant, but just in case.
			if (!this.namedConstants.containsKey(variableName)) {
				throw new RuntimeException("Illegal variable expression: " + node.toString());
			}

			// Retrieve the constant value from named constants map.
			Object value = this.namedConstants.get(variableName);

			// Push the value into the stack top.
			this.executionStack.push(value);
		}
	}

	@Override
	public void caseTNumber(TNumber node) {
		// Parse the number text into a double.
		double value = Double.parseDouble(node.getText());

		// Push the value into the stack top.
		this.executionStack.push(value);
	}

	@Override
	public void caseABooleanExpression(ABooleanExpression node) {
		// Execute the expression leaving the result at the stack top.
		node.getBooleanExpression().apply(this);
	}

	@Override
	public void caseANegationBooleanExpression(ANegationBooleanExpression node) {
		// Execute the underlying expression leaving the result at the stack top.
		node.getBooleanExpression().apply(this);

		// Retrieve the result from the stack top.
		boolean result = this.executionStack.popBoolean();

		// Push the negation of the result into the stack top.
		this.executionStack.push(!result);
	}

	@Override
	public void caseARelationalBooleanExpression(ARelationalBooleanExpression node) {
		// Nothing to push on the stack. The relational expression result
		// will end up at the top of the stack and is the expression result.
		node.getRelationalExpression().apply(this);
	}

	@Override
	public void caseAOrBooleanExpression(AOrBooleanExpression node) {
		// We implement lazy evaluation semantics as in most programming languages.

		// Execute the left boolean expression of the or expression.
		// This execution leaves the result at the stack top.
		node.getLeft().apply(this);

		// Retrieve the result of the left expression from the stack.
		boolean left = this.executionStack.peekBoolean();

		// If left is true there is no need to evaluate the right
		// expression of the or expression to determine its truth value.
		if (!left) {
			// Remove left from the stack, no need to preserve it.
			this.executionStack.pop();

			// The result of evaluating the right
			// expression is the expression result.
			node.getRight().apply(this);
		}
	}

	@Override
	public void caseAAndBooleanExpression(AAndBooleanExpression node) {
		// We implement lazy evaluation semantics as in most programming languages.

		// Execute the left boolean expression of the and expression.
		// This execution leaves the result at the stack top.
		node.getLeft().apply(this);

		// Retrieve the result of the left expression from the stack.
		boolean left = this.executionStack.peekBoolean();

		// If left is false there is no need to evaluate the right
		// expression of the and expression to determine its truth value.
		if (left) {
			// Remove left from the stack, no need to preserve it.
			this.executionStack.pop();

			// The result of evaluating the right
			// clause is the expression result.
			node.getRight().apply(this);
		}
	}

	@Override
	public void caseAEqualRelationalExpression(AEqualRelationalExpression node) {
		// Evaluate both left and right expressions.
		node.getLeft().apply(this);
		node.getRight().apply(this);

		// Pop the result of each expression from the stack.
		double right = this.executionStack.popDouble();
		double left = this.executionStack.popDouble();

		// Push the comparison result into the stack top.
		this.executionStack.push(left == right);
	}

	@Override
	public void caseANotEqualRelationalExpression(ANotEqualRelationalExpression node) {
		// Evaluate both left and right expressions.
		node.getLeft().apply(this);
		node.getRight().apply(this);

		// Pop the result of each expression from the stack.
		double right = this.executionStack.popDouble();
		double left = this.executionStack.popDouble();

		// Push the comparison result into the stack top.
		this.executionStack.push(left != right);
	}

	@Override
	public void caseAGreaterThanRelationalExpression(AGreaterThanRelationalExpression node) {
		// Evaluate both left and right expressions.
		node.getLeft().apply(this);
		node.getRight().apply(this);

		// Pop the result of each expression from the stack.
		double right = this.executionStack.popDouble();
		double left = this.executionStack.popDouble();

		// Push the comparison result into the stack top.
		this.executionStack.push(left > right);
	}

	@Override
	public void caseAGreaterThanOrEqualRelationalExpression(AGreaterThanOrEqualRelationalExpression node) {
		// Evaluate both left and right expressions.
		node.getLeft().apply(this);
		node.getRight().apply(this);

		// Pop the result of each expression from the stack.
		double right = this.executionStack.popDouble();
		double left = this.executionStack.popDouble();

		// Push the comparison result into the stack top.
		this.executionStack.push(left >= right);
	}

	@Override
	public void caseALessThanRelationalExpression(ALessThanRelationalExpression node) {
		// Evaluate both left and right expressions.
		node.getLeft().apply(this);
		node.getRight().apply(this);

		// Pop the result of each expression from the stack.
		double right = this.executionStack.popDouble();
		double left = this.executionStack.popDouble();

		// Push the comparison result into the stack top.
		this.executionStack.push(left < right);
	}

	@Override
	public void caseALessThanOrEqualRelationalExpression(ALessThanOrEqualRelationalExpression node) {
		// Evaluate both left and right expressions.
		node.getLeft().apply(this);
		node.getRight().apply(this);

		// Pop the result of each expression from the stack.
		double right = this.executionStack.popDouble();
		double left = this.executionStack.popDouble();

		// Push the comparison result into the stack top.
		this.executionStack.push(left <= right);
	}

	//
	// The initial frame is special in the sense that we craft it by hand.
	//
	private void createInitialFrame(String parameterName, double value) {
		// Save the current parameter value into the stack.
		this.localsAndParametersStack.push(value);

		// Create a new binding and put the parameter into the binding.
		this.localsBindings.push(new HashMap<String, Integer>());
		this.localsBindings.peek().put(parameterName, PARAMETER_FIRST_INDEX);

		// Save a dummy returned register to create a proper frame.
		this.localsAndParametersStack.push(false);

		// Save base register and create the new frame.
		// There are no locals in the initial frame.
		this.localsAndParametersStack.push(this.bp);
		this.bp = this.localsAndParametersStack.getSP();
	}

	private void allocateFrame(AFunctionDefinition functionDefinition, AFunctionCall call) {
		// Evaluate each function call parameter and leave them on the stack.
		this.pushParameters(call);

		// Create the bindings for the function parameters.
		this.createBindings(functionDefinition);

		// Save the returned register.
		this.localsAndParametersStack.push(this.returned);

		// Save base register and create the new frame.
		this.localsAndParametersStack.push(this.bp);
		this.bp = this.localsAndParametersStack.getSP();

		// Set the next local offset.
		this.nextLocal = LOCAL_FIRST_INDEX;
	}

	private void pushParameters(AFunctionCall call) {
		// Evaluate the parameters. Results end up in the execution stack.
		// Because this version of the interpreter uses a separate parameter
		// stack we need to copy each parameter value from the execution
		// stack into the parameter stack.
		// We need to evaluate the parameters in reverse order because we push
		// function call arguments from right to left.
		List<PNumericExpression> parameters = call.getParameters();

		for (int index = parameters.size() - 1; index >= 0; --index) {
			PNumericExpression parameter = parameters.get(index);

			// Evaluate the parameter.
			parameter.apply(this);

			// Get the value from the top of the execution stack
			// into the parameter stack.
			Object value = this.executionStack.pop();

			this.localsAndParametersStack.push(value);
		}
	}

	private void createBindings(AFunctionDefinition functionDefinition) {
		// Parameter offset contains the current parameter offset in the stack frame.
		this.parameterIndex = PARAMETER_FIRST_INDEX;

		// Create a new binding.
		this.localsBindings.push(new HashMap<String, Integer>());

		// Add each parameter into the binding.
		for (PParameter parameter : functionDefinition.getParameters()) {
			parameter.apply(this);
		}
	}

	private void deallocateInitialFrame() {
		// Restore the base register.
		this.bp = this.localsAndParametersStack.popInteger();

		// Restore the returned register.
		this.returned = this.localsAndParametersStack.popBoolean();

		// The initial frame has only one parameter on the stack.
		this.localsAndParametersStack.pop();

		this.localsBindings.pop();
	}

	private void deallocateFrame(AFunctionDefinition functionDefinition) {
		this.deallocateLocals();

		// Restore the base register.
		this.bp = this.localsAndParametersStack.popInteger();

		// Restore the returned register.
		this.returned = this.localsAndParametersStack.popBoolean();

		// Remove parameters from the stack.
		for (int index = 0; index < functionDefinition.getParameters().size(); ++index) {
			this.localsAndParametersStack.pop();
		}

		this.localsBindings.pop();
	}

	private void deallocateLocals() {
		while (this.localsAndParametersStack.getSP() > this.bp) {
			this.localsAndParametersStack.pop();
		}
	}

	private void initializeNamedConstants() {
		this.namedConstants = new HashMap<>();

		this.namedConstants.put("e", Math.E);
		this.namedConstants.put("pi", Math.PI);

		this.namedConstants.put("true", true);
		this.namedConstants.put("false", false);
	}

	public List<Point> getPoints() {
		return Collections.unmodifiableList(this.points);
	}
}
